using System;
#if NETFX_CORE
using Windows.UI.Xaml;
using System.Reflection;
#else
using System.Windows;
#endif

namespace FreshMeat.Xaml
{
   /// <summary>
   /// Abstract base for attached behaviors.
   /// </summary>
   /// <typeparam name="TAssociated">Type of object to which this behavior can be attached.</typeparam>
   /// <typeparam name="TBehavior">Type of derived behavior.</typeparam>
   public abstract class AttachedBehavior<TBehavior, TAssociated> : AttachedBehavior
      where TBehavior : AttachedBehavior<TBehavior, TAssociated>
      where TAssociated : DependencyObject
   {
      /// <summary>
      /// Gets an instance of this behavior which is attached to the associated object.
      /// </summary>
      /// <param name="associatedObject">The associated object.</param>
      /// <returns>An instance of this behavior.</returns>
      public static TBehavior GetInstance(DependencyObject associatedObject)
      {
         var associatedType = associatedObject.GetType();

         if (!AreCompatibleTypes(typeof(TAssociated), associatedType))
            throw new ArgumentException(string.Format("{0} can only be attached to objects of type {1} (not {2}).", typeof(TBehavior).Name, typeof(TAssociated).FullName, associatedType.FullName), "associatedObject");

         if (IsFakeWindow(associatedType))
            return null; // Bypass normal processing since we're in design mode using a fake window

         return (TBehavior)InternalGetInstance(typeof(TBehavior), associatedObject);
      }

      static bool AreCompatibleTypes(Type expected, Type actual)
      {
#if NETFX_CORE
         if (expected.GetTypeInfo().IsAssignableFrom(actual.GetTypeInfo()))
#else
         if (actual.Equals(expected) || actual.IsSubclassOf(expected))
#endif
            return true;

         // Visual Studio designer uses a fake window instead of a real Window, so we allow the fake window to avoid design-time exceptions.
         if (expected == typeof(Window) && IsFakeWindow(actual))
            return true;

         return false;
      }

      static bool IsFakeWindow(Type associatedType)
      {
         return associatedType.Name == "WindowInstance";
      }

      /// <summary>
      /// Tries to get the object to which the behavior is attached.
      /// If that object has been garbage collected, this will return null.
      /// NEVER store this value in a long-lived location such as a field in your behavior.
      /// Doing so will prevent the associated object from being garbage collected which can 
      /// lead to ever increasing memory usage.  The correct usage is to assign this to a 
      /// local variable that will go out of scope when the call completes, then check if it
      /// is null and if so return immediately; otherwise, do your work and then return.
      /// This will always return a valid object when called from <see cref="AttachedBehavior.OnAttached"/>.
      /// This is not set until after the constructor has been called, so it is not available
      /// in the constructors of derived classes.
      /// </summary>
      public new TAssociated TryGetAssociatedObject()
      {
         return (TAssociated)base.TryGetAssociatedObject();
      }
   }

   /// <summary>
   /// Abstract base for attached behaviors.
   /// Intended for internal use; derive from <see cref="AttachedBehavior{T1,T2}"/> instead.
   /// </summary>
   public abstract class AttachedBehavior
   {
      // Do not publicly expose the registry because that opens up the possibility
      // of the registry methods being called directly, which is not the intended
      // behavior.  This is a private thing accessible only through GetInstance.
      static readonly AttachedBehaviorRegistry Registry = new AttachedBehaviorRegistry();

      protected internal static AttachedBehavior InternalGetInstance(Type behaviorType, DependencyObject associatedObject)
      {
         return Registry.GetInstance(behaviorType, associatedObject);
      }

      WeakReference weakAssociatedObject;

      public void Attach(DependencyObject associatedObject)
      {
         IsDetached = false;
         weakAssociatedObject = new WeakReference(associatedObject);

         OnAttached();
      }

      /// <summary>
      /// Detaches the behavior from the associated object.
      /// Can be called by derived classes when the behavior is no longer wanted.
      /// </summary>
      public void Detach(bool releaseInstance = true)
      {
         if (IsDetached)
            return;

         try
         {
            OnDetaching();
         }
         finally
         {
            IsDetached = true;
            weakAssociatedObject = null;

            if (releaseInstance)
               Registry.ReleaseInstance(this);
         }
      }

      /// <summary>
      /// Tries to get the object to which the behavior is attached.
      /// If that object has been garbage collected, this will return null.
      /// NEVER store this value in a long-lived location such as a field in your behavior.
      /// Doing so will prevent the associated object from being garbage collected which can 
      /// lead to ever increasing memory usage.  The correct usage is to assign this to a 
      /// local variable that will go out of scope when the call completes, then check if it
      /// is null and if so return immediately; otherwise, do your work and then return.
      /// This will always return a valid object when called from <see cref="AttachedBehavior.OnAttached"/>.
      /// This is not set until after the constructor has been called, so it is not available
      /// in the constructors of derived classes.
      /// </summary>
      protected internal DependencyObject TryGetAssociatedObject()
      {
         return weakAssociatedObject != null ? weakAssociatedObject.Target as DependencyObject : null;
      }

      /// <summary>
      /// When implemented in a derived class, typically hooks events on the
      /// object returned by <see cref="TryGetAssociatedObject"/> to implement the attached behavior.
      /// While in this method, TryGetAssociatedObject is guaranteed not to return null.
      /// </summary>
      protected abstract void OnAttached();

      /// <summary>
      /// When implemented in a derived class, typically unhooks events hooked in <see cref="OnAttached"/>.
      /// While in this method, TryGetAssociatedObject is guaranteed not to return null.
      /// </summary>
      protected abstract void OnDetaching();

      /// <summary>
      /// Indicates whether the behavior has already been detached.
      /// </summary>
      protected bool IsDetached { get; private set; }
   }
}